package com.teckteam.ptvsystem.model.facade;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Observable;

import com.teckteam.ptvsystem.controller.JourneyPlanException;
import com.teckteam.ptvsystem.model.AbstractRoute;
import com.teckteam.ptvsystem.model.AbstractStop;
import com.teckteam.ptvsystem.model.DepartureTime;
import com.teckteam.ptvsystem.model.Locality;
import com.teckteam.ptvsystem.model.ResultNode;
import com.teckteam.ptvsystem.model.TimetableSystem;
import com.teckteam.ptvsystem.model.WalkingInstance;
import com.teckteam.ptvsystem.view.LoadingDialog;

/**
 * System class that implements the PTVModel interface.
 * A singleton pattern is used in the design.
 */
public class PTVSystem extends Observable implements PTVModel
{
   private TimetableSystem timetable;
   private int fileLoadCount;
   private File folder, listOfFiles[];
   private FileInputStream gpsRows[];
   private String dbPath = "database", fileName;
   
   private static PTVModel instance;
   
   /**
    * Constructor initializing the timetable system model.
    * If any of the directives terminate, terminate the program.
    * 
    * @throws FileNotFoundException
    */
   private PTVSystem()
   {
      // Start the program for the first time
      folder = new File(dbPath);
      if(!folder.exists())
         folder.mkdir();
      
      listOfFiles = folder.listFiles();
      
      timetable = new TimetableSystem();
      fileLoadCount = 0;   
   }
   
   /**
    * Calculate distance in kilometres from two GPS coordinates
    * using the Haversine Formula
    * 
    * Equation reference: http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates
    * @param sLat
    * @param sLon
    * @param dLat
    * @param dLon
    * @return result
    */
   public double calculateDistance(String sLat, String sLon, String dLat, String dLon)
   {
      //Convert values to radians
      double sLatNum = Math.PI * Double.parseDouble(sLat) / 180,
             sLonNum = Math.PI * Double.parseDouble(sLon) / 180,
             dLatNum = Math.PI * Double.parseDouble(dLat) / 180,
             dLonNum = Math.PI * Double.parseDouble(dLon) / 180,
             theta = dLonNum - sLonNum,
             earthRadius = 6372.797,
             result = Math.acos(Math.sin(sLatNum) * Math.sin(dLatNum) +
                   Math.cos(sLatNum) * Math.cos(dLatNum) * Math.cos(theta)) * earthRadius;
      
      return result;
   }
   
   /**
    * Get list of routes based on transport type
    */
   public ArrayList<String> getAllGroupedRoutes(char transportFlag)
   {
      return timetable.getAllGroupedRoutes(transportFlag);
   }
   
   /**
    * Get list of all stop objects.
    */
   public ArrayList<AbstractStop> getAllStops()
   {
      return timetable.getAllStops();
   }
   
   /**
    * Get list of stops in a group.
    */
   public ArrayList<AbstractStop> getAllGroupedStops(char flag)
   {
      return timetable.getAllGroupedStops(flag);
   }
   
   /**
    * Returns total list of Suburbs.  Will only collect once until reinitialized.
    */
   public ArrayList<String> getAllSuburbs()
   {
      return timetable.getAllSuburbs();
   }
   
   public ArrayList<AbstractStop> getAllSuburbStops(String suburb)
   {
      return timetable.getSuburbStops(suburb);
   }
   
   /**
    * Journey Planner front-end functionality.  After parsing the input form and passing validation,
    * all values are processed to generate an ArrayList of ResultNode objects which we can use to
    * visualise our Journey Plan.
    * 
    * Note: This is probably the most procedural function in the entire source code (other than the AbstractStop constructor)
    */
   public ArrayList<ResultNode> getJourneyPlan(String source, String destination,
         String time, String sLat, String sLon, String dLat, String dLon)
         throws JourneyPlanException
   {
      ArrayList<AbstractStop> 
         stops = new ArrayList<AbstractStop>(),
         interchangeList = new ArrayList<AbstractStop>(),
         endStopBuffer = new ArrayList<AbstractStop>(),
                              
         //Given design of system, collect all instances of stops with the same name.
         sDupeStops = null, dDupeStops = null;
      
      //Hacky compacting
      ArrayList<AbstractRoute>
         sDupeRoutes = null, dDupeRoutes = null;
      
      ArrayList<ResultNode> resultsList = new ArrayList<ResultNode>();
      
      AbstractStop startStop, endStop;
      AbstractRoute startRoute = null, endRoute = null;
      
      
      if(sLat==null && sLon==null)
      {
         startStop = timetable.getStop(source, PTVConstants.TIMETABLE_INBOUND);
         startRoute = timetable.getRouteFromStop(startStop);
         
         sDupeStops = timetable.getStopInstances(source);
         sDupeRoutes = timetable.getPossibleRoutes(sDupeStops);
         
         stops.add(startStop);
      } else
      {
         //used for manual GPS location 
         Locality location = new Locality("someSuburb", sLat, sLon, "99999");
         String inputTime[] = new String[1];
         inputTime[0] = time;
         startStop = new WalkingInstance(inputTime, source, PTVConstants.TIMETABLE_INBOUND, location);
         
         //find nearest stops
         stops.add(startStop);
         startStop = timetable.getNearestStop(startStop);
      }
      
      if(dLat==null && dLon==null)
      {
         endStop = timetable.getStop(destination, PTVConstants.TIMETABLE_OUTBOUND);
         endRoute = timetable.getRouteFromStop(endStop);
         
         dDupeStops = timetable.getStopInstances(destination);
         dDupeRoutes = timetable.getPossibleRoutes(dDupeStops);
         
         endStopBuffer.add(endStop);
         
      } else
      {
         //used for manual GPS location 
         Locality location = new Locality("someSuburb", dLat, dLon, "99999");
         String inputTime[] = new String[1];
         inputTime[0] = time; // This to be replaced later
         endStop = new WalkingInstance(inputTime, destination, PTVConstants.TIMETABLE_OUTBOUND, location);
         
         //find nearest stops
         endStopBuffer.add(timetable.getNearestStop(endStop));
         endStopBuffer.add(endStop);
      }
      
      //Checking if stops are on same route (by name)
      if(checkIfSameRoute(startStop, endStop, startRoute, endRoute))
      {
         if(startRoute.getRouteName().compareTo(endRoute.getRouteName())!=0)
         {
            String s = startRoute.toString().replaceAll("Inbound", "Outbound");
            endRoute = timetable.getRoute(s);
         }
         
         int compare = startRoute.getTravelOrder(
               startStop.toString(), endStop.toString());
         
         //Starting route is inbound by default. if the comparison yields
         //a result less than 0, dictates that it is an outbound journey.  
         if(compare < 0) stops.add(0, endStop);
         else stops.add(endStop);
         
         resultsList = generateResultsList(stops, time);
      } else
      {
//         ArrayList<AbstractStop> check = checkIfSameRoute2(sDupeStops, dDupeStops, sDupeRoutes, dDupeRoutes);
//         startStop = check.get(0);
//         endStop = check.get(1);
         
         // Obtain interchanging points - find common stations between routes
         interchangeList = timetable.getInterchangeList(startRoute, endRoute);
         
         if(interchangeList.size()>2)
         {
            System.out.printf("DEBUG: Found %d interchange stops\n", 
                  interchangeList.size()-2);
            
            //TODO: from here, get quickest route depending on each interchange??
            stops.add(endStop);
            ArrayList<Double> timeCompare = new ArrayList<Double>();
            ArrayList<ArrayList<ResultNode>> comparisons = new ArrayList<ArrayList<ResultNode>>();
            
            for(AbstractStop s : interchangeList)
            {
               stops.add(1, s);
               ArrayList<ResultNode> test = generateResultsList(stops, time);
               comparisons.add(test);
               
               double totalDist = 0.0;
               for (ResultNode r : test)
                  totalDist += r.getDistance();
               
               timeCompare.add(totalDist);
               stops.remove(1);
            }
            
            //Find smallest distance
            double distComp = 0.0;
            int i = 0;
            Double dObj = null;
            for (Double d : timeCompare)
            {
               if (distComp==0 || d.doubleValue() < distComp)
               {
                  distComp = d.doubleValue();
                  System.out.println("Trip distance is "+ distComp);
                  dObj = d;
               }
               i++;
            }
            
            //Return results list with smallest distance
            return comparisons.get(timeCompare.indexOf(dObj));
            
         } else
         {
            System.out.println("DEBUG: interchange not found, extra transportation required");
            //TODO- Logic for transport exchange + walking
            
            System.out.println("TBI: Multi-transport jumps - recursively find nearest stops until an interchange is possible.");
            //Recursively find nearest stops until interchange can be found.
            
         }
      }
      
      return resultsList;
   }
   
   /**
    * DEBUG: Return stops containing same route information
    */
   private ArrayList<AbstractStop> fixStopObjectMapping(ArrayList<AbstractStop> sDupeStops,
         ArrayList<AbstractStop> dDupeStops,
         ArrayList<AbstractRoute> sDupeRoutes,
         ArrayList<AbstractRoute> dDupeRoutes)
   {
      for (AbstractStop start : sDupeStops)
      {
         for (AbstractStop end : dDupeStops)
         {
            for (AbstractRoute sR : sDupeRoutes)
            {
               for (AbstractRoute dR : dDupeRoutes)
               {
                  if(sR.getRouteName().compareTo(dR.getRouteName())==0)
                  {
                     if(sR.getStopIndex(start)!=PTVConstants.FAILED_COMPARE && dR.getStopIndex(start)!=PTVConstants.FAILED_COMPARE
                     && sR.getStopIndex(end)!=PTVConstants.FAILED_COMPARE && dR.getStopIndex(end)!=PTVConstants.FAILED_COMPARE)
                     {
                        ArrayList<AbstractStop> passed = new ArrayList<AbstractStop>();
                        passed.add(start);
                        passed.add(end);
                        System.out.println("Re-matched stops to similar route.");
                        return passed;
                     }
                  }
               }
            }
         }
      }
      
      return null;
   }

   /**
    * Generates a list of ResultNodes based on the stop list collected, and the time.
    * @param stopList
    * @param time
    * @return resultsList
    */
   protected ArrayList<ResultNode> generateResultsList(
         ArrayList<AbstractStop> stopList, String time) throws JourneyPlanException
   {
      ArrayList<ResultNode> resultsList = new ArrayList<ResultNode>();
      int hopExtend = 0;
      
      //Reading stops in order, and deciphering information.
      for (int i = 0; i < stopList.size(); i++)
      {
         if(i > 0)
         {
            //TODO Get interpolated paths for more accuracy
            AbstractStop stopFrom = stopList.get(i-1),
                         stopTo = stopList.get(i);
            
            //Generate distance between nodes (last node is 0 as it is the destination)
            String first[] = stopFrom.getLocality().toArray(),
                   second[] = stopTo.getLocality().toArray();
            
            double distance = calculateDistance(
                  first[0], first[1], second[0], second[1]);
            
            AbstractRoute route = null;
            
            //Ensure this is applicable to only travel on vehicles
            if(!(stopFrom.getTransportType()==PTVConstants.WALK_FLAG
                  || stopTo.getTransportType()==PTVConstants.WALK_FLAG))
               route = timetable.getRouteFromStop(stopFrom);
            
            //Time check
            DepartureTime
               departTime = stopFrom.getNextDeparture(time),
               
               //Get vehicle journey 
               arriveTime = timetable.getArrivalTime(
                     departTime, stopFrom, route, stopTo);
            
            //implement stopover if required (express service to non-express stop)
//            if(PTVConstants.BLANK_TIME.compareTo(departTime.toString()) == 0)
//            {
//               hopExtend++;
//               System.out.println("TBI: HOP EXTEND");
//               //TODO - Find earlier stop on same route
//               
//            } else
               //Continue as normal
            resultsList.add(
                  new ResultNode(stopTo, route, departTime,
                        arriveTime, distance, (i + hopExtend)
            ));                 
         } else
         {
            AbstractStop stopTo = stopList.get(i);
            AbstractRoute route = timetable.getRouteFromStop(stopTo);
            DepartureTime arriveTime = stopTo.getNextDeparture(time);
            resultsList.add(
                  new ResultNode(stopTo, route, arriveTime,
                        arriveTime, 0, (i + hopExtend)
            ));
         }
         
         
      }
      
      int i = 0;
      for (AbstractStop s : stopList)
      {
         System.out.printf("Hop %d: %s\n", ++i, s.toString());
      }
      return resultsList;
   }

   /**
    * Comparing stops to check if they exist on the same route.  Returns true/false.
    * 
    * @param startStop
    * @param endStop
    * @param startRoute
    * @param endRoute
    * @return found
    */
   private boolean checkIfSameRoute(AbstractStop startStop,
         AbstractStop endStop, AbstractRoute startRoute, AbstractRoute endRoute)
   {
      ArrayList<AbstractStop> collectionOfSameStop = new ArrayList<AbstractStop>();
      boolean found = false;
      
      //Basic check
      if (startRoute.getRouteName().compareTo(endRoute.getRouteName()) == 0)
         found = true;
      
      if (!found)
      {
         //Routine to ignore startStop's existence in a possibly different route
         //This will use the destination's route as a reference.
         for(AbstractStop s : endRoute.getStopList())
         {
            if(s.toString().compareTo(startStop.toString()) == 0)
            {
               startStop = s;
               found = true;
            }
         }
      }
      
      if(!found)
      {
         //Routine to ignore endStop's existence in a possibly different route
         //This will use the source's route as a reference.
         for(AbstractStop s : startRoute.getStopList())
         {
            if(s.toString().compareTo(endStop.toString()) == 0)
            {
               endStop = s;
               found = true;
            }
         }
      }
      
      return found;
   }
   
   public HashMap<String, AbstractStop[]> getNearestStops(String name)
   {
      // TODO Auto-generated method stub
      return null;
   }
   
   /**
    * Gets the route timetable after reconstructed to a format similar to the scraped object
    */
   public LinkedHashMap<String, String[]> getRouteTimetable(String name)
   {
      return timetable.getRoute(name).getStopListParsedSource();
   }
   
   /**
    * Debug method: get route timetable from scraped object. TO BE REMOVED
    */
   public LinkedHashMap<String, String[]> getRouteTimetableDebug(String name)
   {
      // TODO comparing functionality - testing grid return
      // Hacky routine
      return timetable.getRouteDebug(name);
   }
   
   public ArrayList<DepartureTime> getStopTimetable(String name)
   {
      // TODO Auto-generated method stub
      return null;
   }
   
   public int importTimetableFiles() throws FileNotFoundException
   {
      LoadingDialog ld = LoadingDialog.getInstance();
      
      //Do not attempt if File pointer finds nothing
      if(listOfFiles==null) return 0;
      
      if(!folder.canRead())
         throw new FileNotFoundException("Cannot read from database folder.");

      //GPS import predefinition - if fails, terminate program with error message
      FileInputStream gpsRows[] = {
            new FileInputStream(String.format("%s/csv/traingps.csv", dbPath)),
            new FileInputStream(String.format("%s/csv/tramgps.csv", dbPath)),
            new FileInputStream(String.format("%s/csv/busgps.csv", dbPath))
      };
      this.gpsRows = gpsRows;
      
      //file loading mechanism
      ld.setLoading(listOfFiles.length);
      for (int i = 0; i < listOfFiles.length; i++)
      {
         if (listOfFiles[i].isFile())
         {
            fileName = listOfFiles[i].getName();
            //Observer communication
            ld.setProgress(i, String.format("Loading %s...", fileName));
            
            if (fileName.endsWith(".ttb") || fileName.endsWith(".TTB"))
            {
               // Load Routine here
               System.err.println("Loading " + fileName);
               
               // Import and validate
               if (readTimetable(listOfFiles[i]))
                  fileLoadCount++;
            }
         }
      }
      ld.setProgress(listOfFiles.length, "");
      injectGPS();
      return fileLoadCount;
   }
   
   /**
    * GPS injection routine from ripped database (credits to Halil Ali)
    */
   private void injectGPS()
   {
      LoadingDialog ld = LoadingDialog.getInstance();
      System.out.println("Injecting GPS data to program");
      try
      {
         int rows[] = {0, 0, 0};
         
         for(int i = 0; i < gpsRows.length; i++)
         {
            ArrayList<Locality> gpsList = new ArrayList<Locality>();
            DataInputStream open = new DataInputStream(gpsRows[i]);
            BufferedReader br = new BufferedReader(new InputStreamReader(open));
            String strLine = br.readLine(); //skip first line
            
            while((strLine = br.readLine())!=null)
            {
               String lineToRead[] = strLine.split(","),
                      
                      //Needed to break apart same location, different stopCode
                      idRead[] = lineToRead[6].split("@");
               
               for(int j = 0; j < idRead.length; j++)
               {
                  gpsList.add(new Locality(
                        lineToRead[2], //suburb
                        lineToRead[3], //latitude
                        lineToRead[4], //longitude
                        idRead[j]  //location code
                  ));
                  rows[i]++;
               }
            }
            open.close();
            ld.setLoading(gpsList.size());
            
            int j = 0;
            for(AbstractStop s : timetable.getAllGroupedStops((char)(PTVConstants.TRAIN_FLAG + i)))      
            {
               ld.setProgress(j, "Importing GPS information...");
               for(Locality l : gpsList)
               {
                  if(s.getStopCode()==l.getStopCode())
                  {
                     ld.setProgress(j++, "Importing GPS information...");
                     s.setLocality(l);
                     break;
                  }
                     
               }
            }
         }
      } catch (IOException e)
      {
         //Nothing
      }
      
      //TODO - Get list of stops without imported locality information
      ArrayList<AbstractStop> failedImportList = new ArrayList<AbstractStop>();
      
      for(AbstractStop s : timetable.getAllStops())
      {
         if(s.getLocality()==null)
         {
            failedImportList.add(s);
            //Find equivalently named stops and attach Locality (hack)
            ArrayList<AbstractStop> st = timetable.getStopInstances(s.toString());
            Locality loc = st.get(0).getLocality();
            if(loc==null)
               for(int i = 1; i < st.size(); i++)
                  loc = st.get(i).getLocality();
            
            if(loc!=null) 
            {
               s.setLocality(loc);
               failedImportList.remove(s);
            }
            
         }
      }
      
      System.out.printf("There are %d empty locality stop objects (vs total of %d)\n",
            failedImportList.size(), timetable.getAllStops().size());
   }
   
   /**
    * Returns true if the timetable system is currently empty/uninitialized
    */
   public boolean isEmpty()
   {
      if (timetable == null || timetable.isEmpty())
         return true;
      
      return false;
   }
   
   /**
    * readTimetable will read a text file and then display the contents of the
    * file (if it is a LinkHashMap object of the specified type <String,
    * String[]>
    * 
    * @param loadedFile
    * 
    */
   public boolean readTimetable(File loadedFile)
   {
      LinkedHashMap<String, String[]> newTimes = new LinkedHashMap<String, String[]>();
      
      try
      {
         ObjectInputStream inputStream = new ObjectInputStream(
               new FileInputStream(loadedFile));
         
         newTimes = (LinkedHashMap<String, String[]>) inputStream.readObject();
         
         inputStream.close();
         
         // DEBUG CONSOLE PRINTS
         /*
          * for (String key : newTimes.keySet()) { String[] value =
          * newTimes.get(key); System.out.println("\nKey = " + key);
          * 
          * for (int i = 0; i < value.length; i++) System.out.print(" " +
          * value[i]); }
          */
         
         try
         {
            timetable.importRouteInfo(newTimes);
         } catch (Exception e)
         {
            e.printStackTrace();
         }
         
      } catch (ClassNotFoundException e)
      {
         e.printStackTrace();
         return false;
      } catch (FileNotFoundException e)
      {
         e.printStackTrace();
         return false;
      } catch (IOException e)
      {
         e.printStackTrace();
         return false;
      } catch (NullPointerException e)
      {
         e.printStackTrace();
         return false;
      }
      
      return true;
      
   }
   
   /**
    * Used to refresh the timetable
    */
   public void refreshTimetable() throws FileNotFoundException
   {
      timetable = new TimetableSystem();
      importTimetableFiles();
   }
   
   /**
    * Save scraped timetable data. Passes resultant object from the Scraper.
    */
   public void saveTimetable(LinkedHashMap<String, String[]> result)
   {
      ObjectOutputStream outputStream = null;
      System.err.println("Saving Timetable");
      File filename, dirTest;
      
      String 
         directory = "database/", 
         file = String.format("%s%s_%s_%s.ttb",
            directory,
            result.get("routeHeader")[3], result.get("routeHeader")[1],
            result.get("routeHeader")[2]);
      
      dirTest = new File(directory);
      if(!dirTest.exists())
         dirTest.mkdir();
      
      filename = new File(file);
      
      try
      {
         outputStream = new ObjectOutputStream(new FileOutputStream(filename));
         outputStream.writeObject(result);
         outputStream.close();
      } catch (FileNotFoundException e)
      {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (IOException e)
      {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
      
   }
   
   /**
    * TO BE REMOVED:  Append timetable object to current system.
    */
   public void updateTimetable(LinkedHashMap<String, String[]> result)
   {
      
      System.err.println(String.format(
            "Adding/Updating timetable for: %s (%s, %s)",
            result.get("routeHeader")[0], result.get("routeHeader")[1],
            result.get("routeHeader")[2]));
      
      /*
       * UPDATE: See PTVConstants.java for list of constants
       * 
       * Key glossary
       * 
       * routeHeader - 0: name of line 1: day flag (T0, T2, UL) 2: inbound flag
       * (H, R) Name of Station - 0..n: Respective departure times. Note: | =
       * express. - = stop not relevant
       */
      
      // TODO LATER: make it work for all modes of transport
      
      saveTimetable(result);
      
      try 
      {
         timetable.importRouteInfo(result);
      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
   
   /**
    * Singleton method to obtain current model instance.
    * @return instance
    */
   public static PTVModel getInstance()
   {
      if(instance==null)
         instance = new PTVSystem();
      
      return instance;
   }
}
